<!DOCTYPE html>
<html lang="zh-CN">





<head>
  <meta charset="UTF-8">
  <link rel="apple-touch-icon" sizes="76x76" href="/img/apple-touch-icon.png">
  <link rel="icon" type="image/png" href="/img/favicon.png">
  <meta name="viewport"
        content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, shrink-to-fit=no">
  <meta http-equiv="x-ua-compatible" content="ie=edge">
  
  <meta name="description" content="安全行业从业者，主要方向PC逆向附带安卓和Linux逆向，时不时喜欢写点BUG代码">
  <meta name="author" content="Cray">
  <meta name="keywords" content="">
  <title>编写基础调试器：第四部分（符号） ~ 逆向安全博客</title>

  <link rel="stylesheet" href="https://cdn.staticfile.org/font-awesome/5.12.1/css/all.min.css"  >
<link rel="stylesheet" href="https://cdn.staticfile.org/twitter-bootstrap/4.4.1/css/bootstrap.min.css"  >
<link rel="stylesheet" href="https://cdn.staticfile.org/mdbootstrap/4.13.0/css/mdb.min.css"  >
<link rel="stylesheet" href="https://cdn.staticfile.org/github-markdown-css/3.0.1/github-markdown.min.css"  >

<link rel="stylesheet" href="//at.alicdn.com/t/font_1067060_qzomjdt8bmp.css">



  <link rel="stylesheet" href="/lib/prettify/tomorrow.min.css"  >

<link rel="stylesheet" href="/css/main.css"  >


  <link rel="stylesheet" href="https://cdn.staticfile.org/fancybox/3.5.7/jquery.fancybox.min.css"  >


<meta name="generator" content="Hexo 5.2.0"></head>


<body>
  <header style="height: 70vh;">
    <nav id="navbar" class="navbar fixed-top  navbar-expand-lg navbar-dark scrolling-navbar">
  <div class="container">


    <button id="navbar-toggler-btn" class="navbar-toggler" type="button" data-toggle="collapse"
            data-target="#navbarSupportedContent"
            aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
      <div class="animated-icon"><span></span><span></span><span></span></div>
    </button>

    <!-- Collapsible content -->
    <div class="collapse navbar-collapse" id="navbarSupportedContent">
      <ul class="navbar-nav ml-auto text-center">
        
          
          
          
          
          <li class="nav-item">
            <a class="nav-link" href="/">首页</a>
          </li>
        
          
          
          
          
          <li class="nav-item">
            <a class="nav-link" href="/archives/">归档</a>
          </li>
        
          
          
          
          
          <li class="nav-item">
            <a class="nav-link" href="/tags/">标签</a>
          </li>
        
          
          
          
          
          <li class="nav-item">
            <a class="nav-link" href="/links/">友链</a>
          </li>
        
        
          <li class="nav-item" id="search-btn">
            <a class="nav-link" data-toggle="modal" data-target="#modalSearch">&nbsp;&nbsp;<i
                class="iconfont icon-search"></i>&nbsp;&nbsp;</a>
          </li>
        
      </ul>
    </div>
  </div>
</nav>

    <div class="view intro-2" id="background" false
      style="background: url('https://gitee.com/cve/BlogImg/raw/master/VM_tools/image-20201123095837741.png') no-repeat center center;
      background-size: cover;">
    
        <div class="full-bg-img">
        <div class="mask rgba-black-light flex-center">
          <div class="container text-center white-text fadeInUp">
            <span class="h2" id="subtitle">
              
                编写基础调试器：第四部分（符号）
              
            </span>

            
              <br>
              

              <p>
                
                  
                  &nbsp;<i class="far fa-chart-bar"></i>
                  <span class="post-count">
                    3k 字
                  </span>&nbsp;
                

                
                  
                  &nbsp;<i class="far fa-clock"></i>
                  <span class="post-count">
                      12 分钟
                  </span>&nbsp;
                

                
                  <!-- 不蒜子统计文章PV -->
                  
                  &nbsp;<i class="far fa-eye" aria-hidden="true"></i>&nbsp;
                  <span id="busuanzi_container_page_pv">
                    <span id="busuanzi_value_page_pv"></span> 次
                  </span>&nbsp;
                
              </p>
            
          </div>

          
        </div>
      </div>
    </div>
  </header>

  <main>
    
      

<div class="container-fluid">
  <div class="row">
    <div class="d-none d-lg-block col-lg-2"></div>
    <div class="col-lg-8 nopadding-md">
      <div class="py-5 z-depth-3" id="board">
        <div class="post-content mx-auto" id="post">
          <div class="markdown-body">
            <p>到目前为止，我们已经开发了调试器，该调试器可以与进程连接和分离，设置和删除断点，打印寄存器和调用堆栈，以及通过更改执行线程上下文来修改控制流。这些都是调试器非常重要的功能。这篇文章的主题是调试符号，更多的是“精打细算”。应用程序可能附带调试符号，也可能不附带调试符号，但是如果确实带有调试符号，即它是您自己的应用程序，则调试过程将变得更加简单。</p>
<h3 id="调试符号"><a href="#调试符号" class="headerlink" title="调试符号"></a>调试符号</h3><p>在最简单的定义下，调试符号是一条信息，它显示了已编译程序的特定部分如何映射回源级别。例如，调试符号可能会告诉您有关内存地址中变量名称的信息，或一系列汇编指令映射到的代码行以及在哪个文件中的信息。它们通常在调试构建期间生成，用于为正在调试（或逆向工程）一段代码的开发人员提供一些清晰度。一种语言没有通用的调试符号格式，并且它们在编译器之间可能会有所不同。在现代Windows平台上，调试符号以<em>程序数据库</em>（<a target="_blank" rel="noopener" href="https://devblogs.microsoft.com/cppblog/whats-inside-a-pdb-file/">PDB</a>）文件的形式出现，以**<em>.pdb**</em>扩展名结尾。</p>
<p>这些文件包含有关已编译的可执行文件或DLL的许多有用信息。如上所述，它们可以包含有关某个地址处的符号映射到哪个源文件和行号（或哪个目标文件）的信息。它们可以包含全局，静态和局部变量的名称和类型，以及类和结构。它们还可以包含在编译代码时使用的信息编译器优化。如果代码使用<a target="_blank" rel="noopener" href="http://msdn.microsoft.com/en-us/library/y87kw2fd.aspx">剥离符号</a>编译，则其中某些内容可能不存在。在调试会话期间，调试器将初始化符号处理程序并开始在公共目录和/或用户指定的目录中递归查找，并<a target="_blank" rel="noopener" href="https://code.google.com/p/pdbparse/">解析</a>匹配的PDB*文件。当用户进行调试时，可以检索符号信息，并可以向他们显示名称和源行号（如果有）。<br>这是一个有用的开源解析器，可以解析PDB文件的专有格式。</p>
<h4 id="实现"><a href="#实现" class="headerlink" title="实现"></a>实现</h4><p>Microsoft通过<strong>DbgHelp API</strong>提供了一组非常丰富的用于处理符号的API。有一些功能可以加载/枚举模块的符号，按名称或地址查找符号，枚举<em>PDB中</em>找到的源文件和行引用，动态添加或删除符号表中的条目，与<a target="_blank" rel="noopener" href="http://msdn.microsoft.com/en-us/library/windows/desktop/ms681417(v=vs.85).aspx">symstore </a>交互等。考虑到非常大的API，我仅选择演示常见功能的实现。有一点要考虑的是，所有<strong>DbgHelp API</strong>集中的函数是单线程的。该示例代码是单线程的，但是没有并发同步来确保仅从单个线程调用它，这意味着如果您要实现的不是此代码，请确保添加并发同步。</p>
<p>初始化符号处理程序非常简单：仅涉及调用<a target="_blank" rel="noopener" href="http://msdn.microsoft.com/en-us/library/windows/desktop/ms681351(v=vs.85).aspx">SymInitialize</a>。该函数具有一个进程句柄，该句柄在附加时由调试器打开。用户搜索路径还有一个参数可以找到<em>PDB</em>文件，第三个参数可以指定调试器是否要枚举进程中所有已加载的模块并加载它们的符号。对于附加调试器，请指定此行为取决于情况。在某些情况下，例如调试器创建了要调试的目标进程，或者使用了<a target="_blank" rel="noopener" href="http://msdn.microsoft.com/en-us/library/yx9zd12s.aspx">延迟加载的DLL</a>，可能会导致某些符号无法加载。此外，如果此第三个参数设置为<em>true</em>并且在接收到所有<em>LOAD_DLL_DEBUG_EVENT</em>事件之前初始化了符号处理程序，则可能不会加载某些符号。实现示例代码已默认设置为<em>false</em>，并且模块符号将加载到**<em>CREATE_PROCESS_DEBUG_EVENT**</em>和<em>LOAD_DLL_DEBUG_EVENT</em>事件处理程序中。这样可以确保正确装载每个模块的所有符号文件。</p>
<p>在初始化符号处理程序之前，应调用<a target="_blank" rel="noopener" href="http://msdn.microsoft.com/en-us/library/windows/desktop/ms681366(v=vs.85).aspx">SymSetOptions</a>函数，该函数配置符号处理程序将加载方式和信息的方式。简单地将其放入代码中，初始化例程如下所示：</p>
<pre><code class="C++">Symbols::Symbols(const HANDLE hProcess, const HANDLE hFile, const bool bLoadAll /*= false*/)
    : m_hProcess&#123; hProcess &#125;, m_hFile&#123; hFile &#125;
&#123;
    (void)SymSetOptions(SYMOPT_CASE_INSENSITIVE | SYMOPT_DEFERRED_LOADS |
        SYMOPT_LOAD_LINES | SYMOPT_UNDNAME);

    const bool bSuccess = BOOLIFY(SymInitialize(hProcess, nullptr, bLoadAll));
    if (!bSuccess)
    &#123;
        fprintf(stderr, &quot;Could not initialize symbol handler. Error = %X.\n&quot;,
            GetLastError());
    &#125;
&#125;        </code></pre>
<p>此处的选项指定符号搜索将不区分大小写，在进行引用之前不会加载符号（不要与上面提到的DLL的延迟加载混淆），将加载行信息以及符号将以未修饰的形式显示。 为了方便起见，不区分大小写和未修饰的名称。 否则，搜索精确的符号名称（如”?f@@YAHD@Z”）会很烦人。</p>
<p>当符号处理程序完成时，即调试器正在从进程中分离时，对SymCleanup的简单调用将终止符号处理程序：</p>
<pre><code class="C++">Symbols::~Symbols()
&#123;
    const bool bSuccess = BOOLIFY(SymCleanup(m_hProcess));
    if (!bSuccess)
    &#123;
        fprintf(stderr, &quot;Could not terminate symbol handler. Error = %X.\n&quot;,
            GetLastError());
    &#125;
&#125;</code></pre>
<p>这就设置好了符号处理程序的初始化和终止。</p>
<h4 id="符号枚举"><a href="#符号枚举" class="headerlink" title="符号枚举"></a>符号枚举</h4><p>调试器的一个有用功能可能是在内部枚举模块的所有符号。 这样可以在以后存储和快速查找。 或者，它可以为用户提供图形显示，并可以从其名称轻松导航到符号地址。 枚举符号是一个两步过程：首先调用<code>SymLoadModuleEx</code>来加载模块的符号表，然后可以使用模块的基地址来调用<code>SymEnumSymbols</code>。 <code>SymEnumSymbols</code>将<code>PSYM_ENUMERATESYMBOLS_CALLBACK</code>类型的回调作为参数。 该回调将针对在模块符号表中找到的每个符号进行调用，并将具有<code>SYMBOL_INFO</code>结构，该结构显示有关该符号的信息，例如其名称，地址，是否为寄存器，如果其为常数则是什么值 等。 在代码中，实现很简单：</p>
<pre><code class="C++">const bool Symbols::EnumerateModuleSymbols(const char * const pModulePath, const DWORD64 dwBaseAddress)
&#123;
    DWORD64 dwBaseOfDll = SymLoadModuleEx(m_hProcess, m_hFile, pModulePath, nullptr,
        dwBaseAddress, 0, nullptr, 0);
    if (dwBaseOfDll == 0)
    &#123;
        fprintf(stderr, &quot;Could not load modules for %s. Error = %X.\n&quot;,
            pModulePath, GetLastError());
        return false;
    &#125;

    UserContext userContext = &#123; this, pModulePath &#125;;
    const bool bSuccess = 
       BOOLIFY(SymEnumSymbols(m_hProcess, dwBaseOfDll, &quot;*!*&quot;, SymEnumCallback, &amp;userContext));
    if (!bSuccess)
    &#123;
        fprintf(stderr, &quot;Could not enumerate symbols for %s. Error = %X.\n&quot;,
            pModulePath, GetLastError());
    &#125;

    return bSuccess;
&#125;</code></pre>
<h4 id="解析符号"><a href="#解析符号" class="headerlink" title="解析符号"></a>解析符号</h4><p>有几种解析符号的方法，但最常见的两种是按名称和按地址。这可以通过分别调用<a target="_blank" rel="noopener" href="http://msdn.microsoft.com/en-us/library/windows/desktop/ms681325(v=vs.85).aspx">SymFromName</a>和<a target="_blank" rel="noopener" href="http://msdn.microsoft.com/en-us/library/windows/desktop/ms681323(v=vs.85).aspx">SymFromAddr</a>来实现。就像调用*<strong>SymEnumSymbols**<em>一样，它们都填充</em></strong>SYMBOL_INFO***结构。调用它们也很简单：</p>
<pre><code class="C++">const bool Symbols::SymbolFromAddress(const DWORD64 dwAddress, const SymbolInfo **pFullSymbolInfo)
&#123;
    char pBuffer[sizeof(SYMBOL_INFO) + MAX_SYM_NAME * sizeof(char)] = &#123; 0 &#125;;
    PSYMBOL_INFO pSymInfo = (PSYMBOL_INFO)pBuffer;

    pSymInfo-&gt;SizeOfStruct = sizeof(SYMBOL_INFO);
    pSymInfo-&gt;MaxNameLen = MAX_SYM_NAME;

    DWORD64 dwDisplacement = 0;
    const bool bSuccess = BOOLIFY(SymFromAddr(m_hProcess, dwAddress, &amp;dwDisplacement, pSymInfo));
    if (!bSuccess)
    &#123;
        fprintf(stderr, &quot;Could not retrieve symbol from address %p. Error = %X.\n&quot;,
            (DWORD_PTR)dwAddress, GetLastError());
        return false;
    &#125;

    fprintf(stderr, &quot;Symbol found at %p. Name: %.*s. Base address of module: %p\n&quot;,
        (DWORD_PTR)dwAddress, pSymInfo-&gt;NameLen, pSymInfo-&gt;Name, (DWORD_PTR)pSymInfo-&gt;ModBase);

    *pFullSymbolInfo = FindSymbolByName(pSymInfo-&gt;Name);

    return bSuccess;
&#125;

const bool Symbols::SymbolFromName(const char * const pName, const SymbolInfo **pFullSymbolInfo)
&#123;
    char pBuffer[sizeof(SYMBOL_INFO) + MAX_SYM_NAME * sizeof(char)
        + sizeof(ULONG64) - 1 / sizeof(ULONG64)] = &#123; 0 &#125;;
    PSYMBOL_INFO pSymInfo = (PSYMBOL_INFO)pBuffer;

    pSymInfo-&gt;SizeOfStruct = sizeof(SYMBOL_INFO);
    pSymInfo-&gt;MaxNameLen = MAX_SYM_NAME;

    const bool bSuccess = BOOLIFY(SymFromName(m_hProcess, pName, pSymInfo));
    if (!bSuccess)
    &#123;
        fprintf(stderr, &quot;Could not retrieve symbol for name %s. Error = %X.\n&quot;,
            pName, GetLastError());
        return false;
    &#125;

    fprintf(stderr, &quot;Symbol found for %s. Name: %.*s. Address: %p. Base address of module: %p\n&quot;,
        pName, pSymInfo-&gt;NameLen, pSymInfo-&gt;Name, (DWORD_PTR)pSymInfo-&gt;Address,
        (DWORD_PTR)pSymInfo-&gt;ModBase);

    *pFullSymbolInfo = FindSymbolByAddress((DWORD_PTR)pSymInfo-&gt;Address);

    return bSuccess;
&#125;</code></pre>
<p>与**<em>SymbolInfo**</em>结构可在伸展结构，具有与源文件和行号的信息（参见实施例的代码）。</p>
<h3 id="测试功能"><a href="#测试功能" class="headerlink" title="测试功能"></a>测试功能</h3><p>为了测试此功能，我们可以从上一篇文章中提取示例程序（如下所示），并查看调用堆栈的外观差异。此版本中的新功能增加了解析调用堆栈中地址符号的功能。此外，调试器得到了增强，增加了两个新功能：转储模块中的所有符号，以及按名称设置/删除符号上的断点。</p>
<pre><code class="C++">#include &lt;cstdio&gt;

void d()
&#123;
    printf(&quot;d called.\n&quot;);
&#125;

void c()
&#123;
    printf(&quot;c called.\n&quot;);
    d();
&#125;

void b()
&#123;
    printf(&quot;b called.\n&quot;);
    c();
&#125;

void a()
&#123;
    printf(&quot;a called.\n&quot;);
    b();
&#125;

int main(int argc, char *argv[])
&#123;
    printf(&quot;Addresses: \n&quot;
        &quot;a: %p\n&quot;
        &quot;b: %p\n&quot;
        &quot;c: %p\n&quot;
        &quot;d: %p\n&quot;,
        a, b, c, d);

    getchar();
    while (true)
    &#123;
        a();
        getchar();
    &#125;

    return 0;
&#125;</code></pre>
<p>在<em>d</em>函数上设置一个断点并打印调用堆栈，显示了以前版本的调试器与该版本之间更有用的功能。输入的命令以红色显示，而新的符号信息以橙色显示。</p>
<blockquote>
<p>原作者博客有颜色，我这里用代码块，颜色不好用，建议产靠原文</p>
</blockquote>
<pre><code class="json">a
[A]ddress or [s]ymbol name? s
Name: d
Received breakpoint at address 00401090.
Press c to continue or s to begin stepping.
l
Frame: 0
Execution address: 00401090
Stack address: 00000000
Frame address: 0018FDE8
Symbol name: d
Symbol address: 00401090
Address displacement: 0
Source file: c:\users\demo\desktop\demoapp\source.cpp
Line number: 4
Frame: 1
Execution address: 0040107C
Stack address: 00000000
Frame address: 0018FDEC
Symbol found at 0040107C. Name: c. Base address of module: 00400000
Symbol name: c
Symbol address: 00401060
Address displacement: 0
Source file: c:\users\demo\desktop\demoapp\source.cpp
Line number: 9
Frame: 2
Execution address: 0040104C
Stack address: 00000000
Frame address: 0018FE40
Symbol found at 0040104C. Name: b. Base address of module: 00400000
Symbol name: b
Symbol address: 00401030
Address displacement: 0
Source file: c:\users\demo\desktop\demoapp\source.cpp
Line number: 15
Frame: 3
Execution address: 0040101C
Stack address: 00000000
Frame address: 0018FE94
Symbol found at 0040101C. Name: a. Base address of module: 00400000
Symbol name: a
Symbol address: 00401000
Address displacement: 0
Source file: c:\users\demo\desktop\demoapp\source.cpp
Line number: 21
Frame: 4
Execution address: 004010EF
Stack address: 00000000
Frame address: 0018FEE8
Symbol found at 004010EF. Name: main. Base address of module: 00400000
Symbol name: main
Symbol address: 004010B0
Address displacement: 0
Source file: c:\users\demo\desktop\demoapp\source.cpp
Line number: 27
Frame: 5
Execution address: 004013A9
Stack address: 00000000
Frame address: 0018FF3C
Symbol found at 004013A9. Name: __tmainCRTStartup. Base address of module: 00400000
Symbol name: __tmainCRTStartup
Symbol address: 00401210
Address displacement: 0
Source file: f:\dd\vctools\crt\crtw32\dllstuff\crtexe.c
Line number: 473
Frame: 6
Execution address: 004014ED
Stack address: 00000000
Frame address: 0018FF8C
Symbol found at 004014ED. Name: mainCRTStartup. Base address of module: 00400000

Symbol name: mainCRTStartup
Symbol address: 004014E0
Address displacement: 0
Source file: f:\dd\vctools\crt\crtw32\dllstuff\crtexe.c
Line number: 456
Frame: 7
Execution address: 76AE919F
Stack address: 00000000
Frame address: 0018FF94
Symbol found at 76AE919F. Name: BaseThreadInitThunk. Base address of module: 00000000
Symbol name: BaseThreadInitThunk
Symbol address: 76AE9191
Address displacement: 0
Source file: (null)
Line number: 0
Frame: 8
Execution address: 77430BBB
Stack address: 00000000
Frame address: 0018FFA0
Symbol found at 77430BBB. Name: RtlInitializeExceptionChain. Base address of module: 00000000
Symbol name: RtlInitializeExceptionChain
Symbol address: 77430B37
Address displacement: 0
Source file: (null)
Line number: 0
Frame: 9
Execution address: 77430B91
Stack address: 00000000
Frame address: 0018FFE4
Symbol found at 77430B91. Name: RtlInitializeExceptionChain. Base address of module: 00000000
Symbol name: RtlInitializeExceptionChain
Symbol address: 77430B37
Address displacement: 0
Source file: (null)
Line number: 0
StackWalk64 finished.</code></pre>
<p>与仅使用绝对地址的上一个版本相比，这看起来更有用。在这里，对于某些符号，可以在主机上找到源文件，并与原始程序集一起呈现给用户。此外，可以为任意模块打印符号，如下所示：</p>
<pre><code class="json">y
Enter in module name to dump symbols for: kernel32.dll
Symbol name: QuirkIsEnabledWorker
Symbol address: 76AE0010
Address displacement: 0
Source file: (null)
Line number: 0
Symbol name: EnumCalendarInfoExEx
Symbol address: 76AE03BD
Address displacement: 0
Source file: (null)
Line number: 0
Symbol name: GetFileMUIPath
Symbol address: 76AE03CE
Address displacement: 0
Source file: (null)
Line number: 0
...</code></pre>
<p>到此结束了有关符号的主题。此处介绍的实现只是**<em>DbgHelp API**</em>方面的可用知识的<em>起点</em>，我建议有兴趣的人员进一步探索有关该主题的MSDN文档。下一篇文章将以调试器通常具有的各种其他功能结束该系列文章。对于那一块，它可能包括以下功能：单步执行代码（当前已实现步骤），向用户展示x86和x64的反汇编列表，并允许修改任意内存，而不仅仅是寄存器和/或线程上下文。</p>
<p>原文链接：<a target="_blank" rel="noopener" href="https://www.codereversing.com/blog/archives/176">https://www.codereversing.com/blog/archives/176</a></p>

            <hr>
          </div>
          <br>
          <div>
            <p>
            
            
              <span>
                <i class="iconfont icon-tag"></i>
                
                  <a class="hover-with-bg" href="/tags/%E9%80%86%E5%90%91/">逆向</a>
                
              </span>
            
            </p>
            
              <p class="note note-warning">本博客所有文章除特别声明外，均采用 <a target="_blank" href="https://zh.wikipedia.org/wiki/Wikipedia:CC_BY-SA_3.0%E5%8D%8F%E8%AE%AE%E6%96%87%E6%9C%AC" rel="nofollow noopener noopener">CC BY-SA 3.0协议</a> 。转载请注明出处！</p>
            
          </div>
        </div>
      </div>
    </div>
    
      <div class="d-none d-lg-block col-lg-2 toc-container">
        <div id="toc">
  <p class="h5"><i class="far fa-list-alt"></i>&nbsp;目录</p>
  <div id="tocbot"></div>
</div>
      </div>
    
  </div>
</div>

<!-- custom -->


<!-- Comments -->
<div class="col-lg-7 mx-auto nopadding-md">
  <div class="container comments mx-auto" id="comments">
    
      <br><br>
      
      
  <div class="disqus" style="width:100%">
    <div id="disqus_thread"></div>
    <script>
      var disqus_config = function () {
        this.page.url = 'http://cve.gitee.io/cve/2020/11/23/编写基础调试器：第四部分（符号）/';
        this.page.identifier = '/2020/11/23/编写基础调试器：第四部分（符号）/';
      };
      var oldLoad = window.onload;
      window.onload = function () {
        var d = document, s = d.createElement('script');
        s.type = 'text/javascript';
        s.src = '//' + 'crayon-1' + '.disqus.com/embed.js';
        s.setAttribute('data-timestamp', +new Date());
        (d.head || d.body).appendChild(s);
      };
    </script>
    <noscript>Please enable JavaScript to view the <a target="_blank" href="https://disqus.com/?ref_noscript" rel="nofollow noopener noopener">comments
        powered by Disqus.</a></noscript>
  </div>


    
  </div>
</div>

    
  </main>

  
    <a class="z-depth-1" id="scroll-top-button" href="#" role="button">
      <i class="fa fa-chevron-up scroll-top-arrow" aria-hidden="true"></i>
    </a>
  

  
    <div class="modal fade" id="modalSearch" tabindex="-1" role="dialog" aria-labelledby="ModalLabel"
     aria-hidden="true">
  <div class="modal-dialog modal-dialog-scrollable modal-lg" role="document">
    <div class="modal-content">
      <div class="modal-header text-center">
        <h4 class="modal-title w-100 font-weight-bold">搜索</h4>
        <button type="button" id="local-search-close" class="close" data-dismiss="modal" aria-label="Close">
          <span aria-hidden="true">&times;</span>
        </button>
      </div>
      <div class="modal-body mx-3">
        <div class="md-form mb-5">
          <input type="text" id="local-search-input" class="form-control validate">
          <label data-error="x" data-success="v"
                 for="local-search-input">关键词</label>
        </div>
        <div class="list-group" id="local-search-result"></div>
      </div>
    </div>
  </div>
</div>
  

  <footer class="mt-5">
  <div class="text-center py-3">
    <a href="https://hexo.io" target="_blank" rel="nofollow noopener"><b>Hexo</b></a>
    <i class="iconfont icon-love"></i>
    <a href="https://github.com/fluid-dev/hexo-theme-fluid" target="_blank" rel="nofollow noopener"> <b>Fluid</b></a>
    <br>

    
  
    <!-- 不蒜子统计PV -->
    
    &nbsp;<span id="busuanzi_container_site_pv"></span>总访问量 
          <span id="busuanzi_value_site_pv"></span> 次&nbsp;
  
  
    <!-- 不蒜子统计UV -->
    
    &nbsp;<span id="busuanzi_container_site_uv"></span>总访客数 
            <span id="busuanzi_value_site_uv"></span> 人&nbsp;
  
  <br>



    


    <!-- cnzz Analytics icon -->
    

  </div>
</footer>

<!-- SCRIPTS -->
<script src="https://cdn.staticfile.org/jquery/3.4.1/jquery.min.js" ></script>
<script src="https://cdn.staticfile.org/popper.js/1.16.1/umd/popper.min.js" ></script>
<script src="https://cdn.staticfile.org/twitter-bootstrap/4.4.1/js/bootstrap.min.js" ></script>
<script src="https://cdn.staticfile.org/mdbootstrap/4.13.0/js/mdb.min.js" ></script>
<script src="/js/main.js" ></script>


  <script src="/js/lazyload.js" ></script>



  
  <script src="https://cdn.staticfile.org/tocbot/4.10.0/tocbot.min.js" ></script>
  <script>
    $(document).ready(function () {
      var navHeight = $('#navbar').height();
      var toc = $('#toc');
      var main = $('main');
      var tocT = navHeight + (toc.offset().top - main.offset().top);
      var tocLimMin = main.offset().top - navHeight;
      var tocLimMax = $('#comments').offset().top - navHeight;
      $(window).scroll(function () {
        var scroH = document.body.scrollTop + document.documentElement.scrollTop;
        if (tocLimMin <= scroH && scroH <= tocLimMax) {
          toc.css({
            'display': 'block',
            'position': 'fixed',
            'top': tocT,
          });
        } else if (scroH <= tocLimMin) {
          toc.css({
            'position': '',
            'top': '',
          });
        } else if (scroH > tocLimMax) {
          toc.css('display', 'none');
        }
      });
      tocbot.init({
        tocSelector: '#tocbot',
        contentSelector: '.post-content',
        headingSelector: 'h1,h2,h3,h4,h5,h6',
        linkClass: 'tocbot-link',
        activeLinkClass: 'tocbot-active-link',
        listClass: 'tocbot-list',
        isCollapsedClass: 'tocbot-is-collapsed',
        collapsibleClass: 'tocbot-is-collapsible',
        scrollSmooth: true,
      });
      if ($('.toc-list-item').length > 0) {
        $('#toc > p').css('visibility', 'visible');
      }
    });
  </script>







  <script async src="//busuanzi.ibruce.info/busuanzi/2.3/busuanzi.pure.mini.js" ></script>


<!-- Plugins -->



  <script src="https://cdn.staticfile.org/prettify/188.0.0/prettify.min.js" ></script>
  <script>
    $(document).ready(function () {
      $('pre').addClass('prettyprint  linenums');
      prettyPrint();
    })
  </script>





  <script src="https://cdn.staticfile.org/anchor-js/4.2.2/anchor.min.js" ></script>
  <script>
    anchors.options = {
      placement: "right",
      visible: "hover",
      
    };
    var el = "h1,h2,h3,h4,h5,h6".split(",");
    var res = [];
    for (item of el) {
      res.push(".markdown-body > " + item)
    }
    anchors.add(res.join(", "))
  </script>



  <script src="/js/local-search.js" ></script>
  <script>
    var path = "/local-search.xml";
    var inputArea = document.querySelector("#local-search-input");
    inputArea.onclick = function () {
      getSearchFile(path);
      this.onclick = null
    }
  </script>



  <script src="https://cdn.staticfile.org/fancybox/3.5.7/jquery.fancybox.min.js" ></script>
  <script>
    $("#post img:not(.no-zoom img, img[no-zoom])").each(
      function () {
        var element = document.createElement("a");
        $(element).attr("data-fancybox", "images");
        $(element).attr("href", $(this).attr("src"));
        $(this).wrap(element);
      }
    );
  </script>












</body>
</html>
